/*
 * Copyright (C) 2010 ST-Ericsson AS
 * Author: Erwan Bracq / erwan.bracq@stericsson.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 */

#include <mid_caif.h>

#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <assert.h>
#include <unistd.h>
#include <stdarg.h>

#include <mid.h>
#include <mid_log.h>
#include <mid_error.h>
#include <mid_statemachine.h>
#include <mid_caif_transport.h>
#include <atchannel.h>
#include <at_tok.h>
#include <misc.h>
#include <mid_ipc.h>
#include <mid_settings.h>

#define MAX_AT_STR_SZ	256

static struct mid *my_midinfo = NULL;

static inline void lock_mid_data(void)
{
	apr_status_t status;
	if (unlikely((status = apr_thread_mutex_lock(my_midinfo->mutex)) != APR_SUCCESS)) {
		char message[100];
		MLGE("MID: Unable to lock internal MID data mutex: %s.",
				apr_strerror(status, message, sizeof(message)));
	}
}

static inline void unlock_mid_data(void)
{
	apr_status_t status;
	if (unlikely((status = apr_thread_mutex_unlock(my_midinfo->mutex)) != APR_SUCCESS)) {
		char message[100];
		MLGE("MID: Unable to unlock internal MID data mutex: %s.",
				apr_strerror(status, message, sizeof(message)));
	}
}

#define LOCK_MID_DATA	lock_mid_data()
#define UNLOCK_MID_DATA	unlock_mid_data()
#define TRYLOCK_MID_DATA \
    if (unlikely(apr_thread_mutex_trylock(my_midinfo->mutex) != APR_SUCCESS)) \
	    MLGD("MID: Internal MID data mutex - already locked."); \
    else \
	    locked = 1;


static void mid_at_chnl_onATTimeout()
{
	int locked = 0;
	TRYLOCK_MID_DATA;
	/* Timeout occured, so something is wrong on the AT channel,
	 * so we have to close it.
	 */
	mid_at_chnl_close(my_midinfo);

	/* It s time to restart the modem. */
	SET_FLAG(my_midinfo->event, MID_IPC_RBT_EVT);
	if (likely(locked))
		UNLOCK_MID_DATA;
	return;
}

static void mid_at_chnl_onunsolicited(const char *s, const char *sms_pdu)
{
	union ipc_msg msg;
	int locked = 0;
	int *fd = &(my_midinfo->at_chnl_fd);

	if (sms_pdu != NULL) {
		MLGD("AT: Received unsolicited SMS PDU traffic: %s - Discarded, as the MID does not process those data.", sms_pdu);
		return;
	}

	if (my_midinfo == NULL) {
		MLGE("AT: Received unsolicited traffic before opening AT-Channel. This should not be and unexpected problem could occur from now on MID AT-Channel.");
		return;
	}

	TRYLOCK_MID_DATA;

	if (strStartsWith(s, "*EFWD: ")) {
		int res = 0;
		int chnl = -1;
		int cfunval = -1;
		char *cfunvalstrg = NULL;
		char *tok;
		char *line = tok = strdup(s);
		char *fwdcmd;
		int written;

		res = at_tok_start(&line);
		if (res < 0) {
			free(tok);
			goto error_exit;
		}

		res = at_tok_nextint(&line, &chnl);
		if (res < 0) {
			free(tok);
			goto error_exit;
		}

		res = at_tok_nextstr(&line, &fwdcmd);
         	if (res < 0) {
			free(tok);
			goto error_exit;
		}

		MLGD("AT: <<< Received *EFWD string: %s for AT channel id: %d with command: %s.", line, chnl, fwdcmd);
		if (strcasestr(fwdcmd, "+CFUN=?") != NULL) {
			asprintf(&(my_midinfo->at_cfun_resp), "AT*EFWDR=%d,1\r\n", chnl);
			/* CFUN=? command. */
			MLGD("AT: >>> Forward command reply for CFUN=?: %s.", my_midinfo->at_cfun_resp);
			/* Allowed to forward the command */
			written = write(*fd, my_midinfo->at_cfun_resp, strlen(my_midinfo->at_cfun_resp));
			if (written != (int)strlen(my_midinfo->at_cfun_resp)) {
				free(tok);
				free(my_midinfo->at_cfun_resp);
				goto error_exit;
			}
			free(my_midinfo->at_cfun_resp);
			free(tok);
			goto exit;
		}
		if (strcasestr(fwdcmd, "+CFUN=") != NULL) {
			/* CFUN= command. Needs to take the correct value */
			/* Search for = character */
			cfunvalstrg = index(fwdcmd, 0x3d);
			cfunvalstrg++;
			cfunval = atoi(cfunvalstrg);
			switch (cfunval) {
			case 0:
				/* Execute reboot */
				/* The EFWDR reply (OK) is sent a little bit later */
				asprintf(&(my_midinfo->at_cfun0_resp), "AT*EFWDR=%d,0\r\n", chnl);
				MLGD("STMACH: MID_AT_CFUN0_EVT event set.");
				SET_FLAG(my_midinfo->event, MID_AT_CFUN0_EVT);
				break;
			case 100:
				/* Execute shutdown */
				/* The EFWDR reply (OK) is sent a little bit later */
				asprintf(&(my_midinfo->at_cfun100_resp), "AT*EFWDR=%d,0\r\n", chnl);
				MLGD("STMACH: MID_AT_CFUN100_EVT event set.");
				SET_FLAG(my_midinfo->event, MID_AT_CFUN100_EVT);
				break;
			default:
				/* Allowed to forward the command */
				asprintf(&(my_midinfo->at_cfun_resp), "AT*EFWDR=%d,1\r\n", chnl);
				MLGD("AT: >>> Forward command reply for CFUN=: %s.", my_midinfo->at_cfun_resp);
				written = write(*fd, my_midinfo->at_cfun_resp, strlen(my_midinfo->at_cfun_resp));
				if (written != (int)strlen(my_midinfo->at_cfun_resp)) {
					free(tok);
					free(my_midinfo->at_cfun_resp);
					goto error_exit;
				}
				free(my_midinfo->at_cfun_resp);
				break;
			}
			free(tok);
			goto exit;
		}
		if (strcasestr(fwdcmd, "+CFUN?") != NULL) {
			/* CFUN? command. */
			asprintf(&(my_midinfo->at_cfun_resp), "AT*EFWDR=%d,1\r\n", chnl);
			MLGD("AT: >>> Forward command reply for CFUN?: %s.", my_midinfo->at_cfun_resp);
			/* Allowed to forward the command */
			written = write(*fd, my_midinfo->at_cfun_resp, strlen(my_midinfo->at_cfun_resp));
			if (written != (int)strlen(my_midinfo->at_cfun_resp)) {
				free(tok);
				free(my_midinfo->at_cfun_resp);
				goto error_exit;
			}
			free(my_midinfo->at_cfun_resp);
			free(tok);
			goto exit;
		}
		free(tok);
		goto exit;
	}

	if ((my_midinfo->state == STATE_BOOT) || (my_midinfo->state == STATE_MODINF_UP)) {
		if (strStartsWith(s, "*EMRDY")) {
			char *tok;
			char *line = tok = strdup(s);
			if ((strlen(line) == 9) && (strncmp(line, "*EMRDY: 1", 9) == 0)) {
				MLGD("AT: <<< Received string:\"*EMRDY: 1\". AT IS READY.");
				MLGD("STMACH: MID_CFRDY_EVT event set.");
				SET_FLAG(my_midinfo->event, MID_CFRDY_EVT);
				free(tok);
				goto exit;
			} else {
				free(tok);
				goto error_exit;
			}
		}
	}

	if (strStartsWith(s, "*ETMP:")) {
		int res = 0;
		int temperature_value;
		char *tok;
		char *line = tok = strdup(s);

		res = at_tok_start(&line);
		if (res < 0) {
			free(tok);
			goto error_exit;
		}
		MLGD("AT: <<< Received *ETMP string: %s.", line);

		res = at_tok_nextint(&line, &temperature_value);
		if (res < 0) {
			free(tok);
			goto error_exit;
		}

		/* Receiving this regardless of temperature indicates that the temperature is to high */
		MLGW("MID: Received high battery temperature warning(%d) on modem", temperature_value);

		ipc_prepare_message(&msg, MODEMWARN, "high_temperature");
		ipc_send_message(msg);

		MLGD("STMACH: MID_HIGH_TEMPERATURE_EVT event set.");
		SET_FLAG(my_midinfo->event, MID_HIGH_TEMPERATURE_EVT);
		goto exit;
	}

	if (strStartsWith(s, "*EBAT:")) {
		int res = 0;
		int battery_evt;
		char *tok;
		char *line = tok = strdup(s);

		res = at_tok_start(&line);
		if (res < 0) {
			free(tok);
			goto error_exit;
		}
		MLGD("AT: <<< Received *EBAT string: %s.", line);

		res = at_tok_nextint(&line, &battery_evt);
		if (res < 0) {
			free(tok);
			goto error_exit;
		}

		/* 5 = battery cut off voltage */
		if (battery_evt == 5) {
			MLGW("MID: Received \"cut battery\" voltage warning on modem.");
			ipc_prepare_message(&msg, MODEMWARN, "cut_voltage");
			ipc_send_message(msg);
			MLGD("STMACH: MID_LOW_BATTERY_EVT event set.");
			SET_FLAG(my_midinfo->event, MID_LOW_BATTERY_EVT);
		}
		/* 7 = battery low voltage */
		if (battery_evt == 7) {
			MLGW("MID: Received \"low battery\" voltage warning on modem");
			ipc_prepare_message(&msg, MODEMWARN, "low_voltage");
			ipc_send_message(msg);
		}
		goto exit;
	}

error_exit:
	if (!strStartsWith(s, "OK"))
		MLGD("AT: <<< Non handled AT traffic: %s - Safely discarded.", s);
exit:
	if (likely(locked))
		UNLOCK_MID_DATA;
	return;
}

int mid_at_chnl_open(struct mid *midinfo) {
	int *fd = &(midinfo->at_chnl_fd);
	int ret;

	ret = mid_caif_transport_open(fd);
	if (ret != 0)
		goto cleanup;

	/* Store the midinfo pointer for later usage on mid_at_chnl_onunsolicited */
	my_midinfo = midinfo;

	if (unlikely(at_open(*fd, mid_at_chnl_onunsolicited) == -1)) {
		MLGE("AT: Failed to open AT-Channel.");
		goto cleanup_socket;
	} else {
		at_set_on_timeout(mid_at_chnl_onATTimeout);
		/* MID commands should be fast, as no complex operation are requested
		 * so the timeout is low - see mid_settings.h */
		at_set_timeout_msec(MID_AT_CHN_TIMEOUT * 1000);
		at_make_default_channel();
	}

	/* For non *EMRDY enabled modem, we use rfm_delay and
	 * set the AT ready event here */
	if (midinfo->config->modem_arch_id == OSMIUM) {
		MLGD("STMACH: MID_CFRDY_EVT event set.");
		SET_FLAG(my_midinfo->event, MID_CFRDY_EVT);
	}

	return 0;

cleanup_socket:
	close(*fd);
	*fd = -1;
cleanup:
	my_midinfo = NULL;
	return -EMIDAT;
}

void mid_at_chnl_close(struct mid *midinfo) {
	if (midinfo->at_chnl_fd < 0) {
		MLGD("AT: AT channel already closed.");
		return;
	}
	MLGD("AT: Closing AT channel (fd:%d).", midinfo->at_chnl_fd);
	at_close();
	close(midinfo->at_chnl_fd);
	midinfo->at_chnl_fd = -1;
}

int mid_configure_at_chnl(struct mid *midinfo) {
	int res = 0;
	ATResponse *atresp = NULL;
	ATLine *atline;
	char *line;
	int str_sz;

	/* Configure/set
     	 *   command echo (E), result code suppression (Q), DCE response format (V)
     	 *
     	 *  E0 = DCE does not echo characters during command state and online
     	 *       command state
         *  Q0 = DCE transmits result codes
         *  V1 = Display verbose result codes
	 *  10 seconds timeout.
         */
	res = at_send_command_with_timeout("ATE0", &atresp, 10 * 1000);
	if (res < 0 || atresp->success == 0) {
		MLGE("AT: >>> Failed to sent ATE0 command. AT channel configuration aborted.");
		if (atresp != NULL)
			at_response_free(atresp);
		return -1;
	}
	if (atresp != NULL)
		at_response_free(atresp);

    	/* Set default character set.
	 * 10 seconds timeout.
	 */
    	res = at_send_command_with_timeout("AT+CSCS=\"UTF-8\"", &atresp, 10 * 1000);
	if (res < 0 || atresp->success == 0)
		MLGE("AT: >>> Failed to sent AT+CSCS=\"UTF-8\" command. This is not critical.");
	if (atresp != NULL)
		at_response_free(atresp);

	/* Enable +CME ERROR: <err> result code and use numeric <err> values.
	 * 10 seconds timeout.
	 */
	res = at_send_command_with_timeout("AT+CMEE=1", &atresp, 10 * 1000);
	if (res < 0 || atresp->success == 0) {
		MLGE("AT: >>> Failed to sent AT+CMEE=1 command. AT channel configuration aborted.");
		if (atresp != NULL)
			at_response_free(atresp);
		return -1;
	}
	if (atresp != NULL)
		at_response_free(atresp);

	/* Try to identify prepare for shutdown availibility.
	 * 10 seconds timeout.
	 */
	midinfo->prep_for_shutdown = 0;
	res = at_send_command_singleline_with_timeout("AT+CFUN=?", "+CFUN:", &atresp, 10 * 1000);
	if (res < 0 || atresp->success == 0)
		MLGE("AT: >>> Failure on AT+CFUN=? command. This should not be and unexpected problem could occur from now for modem power down operation.");
	if (atresp != NULL) {
		line = atresp->p_intermediates->line;
		res = at_tok_start(&line);
		if (res < 0)
			MLGE("AT: >>> Failure on AT+CFUN=? command. This should not be and unexpected problem could occur from now for modem power down operation.");
		else {
			if (strstr(line, "100") != NULL) {
				MLGD("MID: Modem support prepare for shutdown command.");
				midinfo->prep_for_shutdown = 1;
			} else
				MLGD("MID: Modem does NOT support prepare for shutdown command.");
		}
		at_response_free(atresp);
	}

	if (midinfo->config->enable_efwd_atcfun) {
		/* AT CFUN forwarding subscribtion
	 	 * 10 seconds timeout.
		 */
		res = at_send_command_with_timeout("AT*EFWD=1,\"+CFUN\",255", &atresp, 10 * 1000);
		if (res < 0 || atresp->success == 0)
			MLGD("MID: Modem does NOT support forward command. AT+CFUN commands will not be intercepted by MID.");
		if (atresp != NULL)
			at_response_free(atresp);
	}

	if (midinfo->config->enable_low_battery_subscription) {
		MLGD("MID: Subscribe for battery low level notification.");
	 	 /* 10 seconds timeout. */
		res = at_send_command_with_timeout("AT*EBAT=1", NULL, 10 * 1000);
		if (res < 0)
			MLGD("MID: Modem does NOT support battery notification subscription command. MID will not react on low battery level.");
	}

	if (midinfo->config->enable_high_temperature_subscription) {
		MLGD("MID: Subscribe for high temperature notification.");
	 	 /* 10 seconds timeout. */
		res = at_send_command_with_timeout("AT*ETMP=1", NULL, 10 * 1000);
		if (res < 0)
			MLGD("MID: Modem does NOT support temperature notification subscription command. MID will not react on high temperature level.");
	}

	if (midinfo->config->enable_atcgmr) {
		/* Modem build ID. */
		res = at_send_command_with_echo("AT+CGMR", &atresp);
		if (res < 0 || atresp->success == 0 || atresp->p_intermediates == NULL) {
			MLGE("AT: >>> AT+CGMR error. Not fatal, but unable to get modem build id.");
			if (atresp != NULL)
				at_response_free(atresp);
			/* This is not an error. Even if we don t have the build ID, the AT channel is now configured */
			return 0;
		}
		for (atline = atresp->p_intermediates; atline->p_next; atline = atline->p_next) {
			MLGD("AT: Skipping local echo for AT+CGMR.");
		}
		line = atline->line;
		str_sz = strlen(line);
		my_midinfo->modem_build_string = malloc((str_sz + 1) * sizeof(char));
		strncpy(my_midinfo->modem_build_string, (const char*)line, str_sz);
		my_midinfo->modem_build_string[str_sz] = '\0';

		MLGD("MID: Modem build ID: %s.", my_midinfo->modem_build_string);

		if (atresp != NULL)
			at_response_free(atresp);
	}

	return 0;
}
